/** * Copyright (c) 2014, the Railo Company Ltd. * Copyright (c) 2015, Lucee Assosication Switzerland * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see <http://www.gnu.org/licenses/>. * */ package lucee.runtime.instrumentation; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.instrument.Instrumentation; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.security.AccessController; import java.security.PrivilegedAction; import lucee.commons.io.IOUtil; import lucee.commons.io.SystemUtil; import lucee.commons.io.log.Log; import lucee.commons.io.res.Resource; import lucee.commons.io.res.ResourcesImpl; import lucee.commons.io.res.type.file.FileResource; import lucee.commons.io.res.util.ResourceUtil; import lucee.commons.lang.ClassUtil; import lucee.commons.lang.ExceptionUtil; import lucee.commons.lang.types.RefBoolean; import lucee.commons.lang.types.RefBooleanImpl; import lucee.loader.engine.CFMLEngine; import lucee.loader.engine.CFMLEngineFactory; import lucee.runtime.config.Config; import lucee.runtime.config.ConfigWebUtil; import lucee.runtime.config.Constants; import lucee.runtime.engine.InfoImpl; import lucee.runtime.exp.ApplicationException; import lucee.runtime.exp.PageRuntimeException; /** * Factory for obtaining an {@link Instrumentation} instance. */ public class InstrumentationFactory { private static final String _name = InstrumentationFactory.class.getName(); private static final String SEP = File.separator; private static final String TOOLS_VERSION = "7u25"; private static final String AGENT_CLASS_NAME = "lucee.runtime.instrumentation.ExternalAgent"; private static Instrumentation _instr; public static synchronized Instrumentation getInstrumentation(final Config config) { final Log log=config.getLog("application"); final CFMLEngine engine = ConfigWebUtil.getEngine(config); Instrumentation instr=_getInstrumentation(log,config); // agent already exist if (instr!=null) return instr; AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { ClassLoader ccl = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader()); try{ JavaVendor vendor = JavaVendor.getCurrentVendor(); Resource toolsJar = null; // When running on IBM, the attach api classes are packaged in vm.jar which is a part // of the default vm classpath. RefBoolean useOurOwn=new RefBooleanImpl(true); //if (!vendor.isIBM()) { // If we can't find the tools.jar and we're not on IBM we can't load the agent. toolsJar = findToolsJar(config,log,useOurOwn); if (toolsJar == null) { return null; } //} log.info("Instrumentation","tools.jar used:"+toolsJar); // add the attach native library if(useOurOwn.toBooleanValue()) addAttachIfNecessary(config,log); Class<?> vmClass = loadVMClass(toolsJar, log, vendor); log.info("Instrumentation","loaded VirtualMachine class:"+(vmClass==null?"null":vmClass.getName())); if (vmClass == null) { return null; } String agentPath = createAgentJar(log,config).getAbsolutePath(); if (agentPath == null) { return null; } log.info("Instrumentation","try to load agent (path:"+agentPath+")"); loadAgent(config,log, agentPath, vmClass); //log.info("Instrumentation","agent loaded (path:"+agentPath+")"); } catch(IOException ioe){ log.log(Log.LEVEL_INFO,"Instrumentation", ioe); } finally{ Thread.currentThread().setContextClassLoader(ccl); } return null; }// end run() }); // If the load(...) agent call was successful, this variable will no // longer be null. instr=_getInstrumentation(log, config); if(instr==null) { try{ Resource agentJar = createAgentJar(log,config); throw new PageRuntimeException(new ApplicationException(Constants.NAME+" was not able to load a Agent dynamically! " + "You need to load one manually by adding the following to your JVM arguments [-javaagent:\""+(agentJar)+"\"]")); } catch(IOException ioe){ioe.printStackTrace();} } return instr; } private static Instrumentation _getInstrumentation(Log log, Config config) { if(_instr!=null) return _instr; // try to get from different Classloaders _instr=_getInstrumentation(ClassLoader.getSystemClassLoader(),log); if(_instr!=null) return _instr; _instr=_getInstrumentation(CFMLEngineFactory.class.getClassLoader(),log); if(_instr!=null) return _instr; _instr=_getInstrumentation(config.getClassLoader(),log); return _instr; } private static Instrumentation _getInstrumentation(ClassLoader cl,Log log) { // get Class Class<?> clazz=ClassUtil.loadClass(cl,AGENT_CLASS_NAME, null); if(clazz!=null) { log.info("Instrumentation", "found [lucee.runtime.instrumentation.ExternalAgent] in ClassLoader ["+clazz.getClassLoader()+"]"); } else { log.info("Instrumentation", "not found [lucee.runtime.instrumentation.ExternalAgent] in ClassLoader ["+cl+"]"); return null; } try { Method m = clazz.getMethod("getInstrumentation", new Class[0]); _instr=(Instrumentation) m.invoke(null, new Object[0]); log.info("Instrumentation", "ExternalAgent does "+(_instr!=null?"":"not ")+"contain a Instrumentation instance"); return _instr; } catch(Throwable t){ ExceptionUtil.rethrowIfNecessary(t); log.log(Log.LEVEL_INFO, "Instrumentation", t); } return null; } private static Resource createAgentJar(Log log,Config c) throws IOException { Resource trg = getDeployDirectory(c).getRealResource("lucee-external-agent.jar"); if(!trg.exists() || trg.length()==0) { log.info("Instrumentation", "create "+trg); InputStream jar = InfoImpl.class.getResourceAsStream("/resource/lib/lucee-external-agent.jar"); if(jar==null) { throw new IOException("could not load jar [/resource/lib/lucee-external-agent.jar]"); } IOUtil.copy(jar, trg,true); } return trg; } private static Resource createToolsJar(Config config) throws IOException { Resource dir=getDeployDirectory(config); String os="bsd"; // used for Mac OS X if(SystemUtil.isWindows()) { os="windows"; } else if(SystemUtil.isLinux()) { // not MacOSX os="linux"; } else if(SystemUtil.isSolaris()) { os="solaris"; } String name="tools-"+os+"-"+TOOLS_VERSION+".jar"; Resource trg = dir.getRealResource(name); if(!trg.exists() || trg.length()==0) { InputStream jar = InfoImpl.class.getResourceAsStream("/resource/lib/"+name); IOUtil.copy(jar, trg,true); } return trg; } private static Resource getDeployDirectory(Config config) { Resource dir=ConfigWebUtil.getConfigServerDirectory(config); if(dir==null || !dir.isWriteable() || !dir.isReadable()) dir= ResourceUtil.toResource(CFMLEngineFactory.getClassLoaderRoot(SystemUtil.getLoaderClassLoader())); return dir; } private static Resource getBinDirectory(Config config) { Resource dir=ConfigWebUtil.getConfigServerDirectory(config); if(dir==null || !dir.isWriteable() || !dir.isReadable()) dir= ResourceUtil.toResource(CFMLEngineFactory.getClassLoaderRoot(SystemUtil.getLoaderClassLoader())); else { dir=dir.getRealResource("bin"); if(!dir.exists()) dir.mkdir(); } return dir; } /** * This private worker method attempts to find [java_home]/lib/tools.jar. * Note: The tools.jar is a part of the SDK, it is not present in the JRE. * * @return If tools.jar can be found, a File representing tools.jar. <BR> * If tools.jar cannot be found, null. */ private static Resource findToolsJar(Config config,Log log, RefBoolean useOurOwn) { log.info("Instrumentation","looking for tools.jar"); String javaHome = System.getProperty("java.home"); Resource javaHomeFile = ResourcesImpl.getFileResourceProvider().getResource(javaHome); Resource toolsJarFile = javaHomeFile.getRealResource("lib" + File.separator + "tools.jar"); if(toolsJarFile.exists()) { useOurOwn.setValue(false); return toolsJarFile; } log.info("Instrumentation","couldn't find tools.jar at: " + toolsJarFile.getAbsolutePath()); // If we're on an IBM SDK, then remove /jre off of java.home and try again. if (javaHomeFile.getAbsolutePath().endsWith(SEP + "jre")) { javaHomeFile = javaHomeFile.getParentResource(); toolsJarFile = javaHomeFile.getRealResource( "lib" + SEP + "tools.jar"); if (!toolsJarFile.exists()) { log.info("Instrumentation","for IBM SDK couldn't find " + toolsJarFile.getAbsolutePath()); } else { useOurOwn.setValue(false); return toolsJarFile; } } else if (System.getProperty("os.name").toLowerCase().indexOf("mac") >= 0) { // If we're on a Mac, then change the search path to use ../Classes/classes.jar. if (javaHomeFile.getAbsolutePath().endsWith(SEP + "Home")) { javaHomeFile = javaHomeFile.getParentResource(); toolsJarFile = javaHomeFile.getRealResource("Classes" + SEP + "classes.jar"); if (!toolsJarFile.exists()) { log.info("Instrumentation","for Mac OS couldn't find " + toolsJarFile.getAbsolutePath()); } else { useOurOwn.setValue(false); return toolsJarFile; } } } // if the engine could not find the tools.jar it is using it's own version try { toolsJarFile=createToolsJar(config); } catch (IOException e) { log.log(Log.LEVEL_INFO,"Instrumentation", e); } if (!toolsJarFile.exists()) { log.info("Instrumentation","could not be created " + toolsJarFile.getAbsolutePath()); return null; } log.info("Instrumentation","found " + toolsJarFile.getAbsolutePath()); return toolsJarFile; } /** * Attach and load an agent class. * * @param log Log used if the agent cannot be loaded. * @param agentJar absolute path to the agent jar. * @param vmClass VirtualMachine.class from tools.jar. */ private static void loadAgent(Config config,Log log, String agentJar, Class<?> vmClass) { try { //addAttach(config,log); // first obtain the PID of the currently-running process // ### this relies on the undocumented convention of the // RuntimeMXBean's // ### name starting with the PID, but there appears to be no other // ### way to obtain the current process' id, which we need for // ### the attach process RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); String pid = runtime.getName(); if (pid.indexOf("@") != -1) pid = pid.substring(0, pid.indexOf("@")); log.info("Instrumentation","pid:"+pid); // JDK1.6: now attach to the current VM so we can deploy a new agent // ### this is a Sun JVM specific feature; other JVMs may offer // ### this feature, but in an implementation-dependent way Object vm = vmClass.getMethod("attach", new Class<?>[] { String.class }) .invoke(null, new Object[] { pid }); // now deploy the actual agent, which will wind up calling // agentmain() vmClass.getMethod("loadAgent", new Class[] { String.class }) .invoke(vm, new Object[] { agentJar }); vmClass.getMethod("detach", new Class[] {}).invoke(vm, new Object[] {}); } catch(Throwable t) { ExceptionUtil.rethrowIfNecessary(t); // Log the message from the exception. Don't log the entire // stack as this is expected when running on a JDK that doesn't // support the Attach API. log.log(Log.LEVEL_INFO,"Instrumentation",t); } } private static void addAttachIfNecessary(Config config, Log log) { String srcName=null,trgName=null; String archBits = (SystemUtil.getJREArch() == SystemUtil.ARCH_64) ? "64" : "32"; // Windows if (SystemUtil.isWindows()) { trgName = "attach.dll"; srcName = "windows" + archBits + "/" + trgName; } // Linux else if (SystemUtil.isLinux()) { trgName = "libattach.so"; srcName = "linux" + archBits + "/" + trgName; } // Solaris else if (SystemUtil.isSolaris()) { trgName = "libattach.so"; srcName = "solaris" + archBits + "/" + trgName; } // Mac OSX else if (SystemUtil.isMacOSX()) { trgName = "libattach.dylib"; srcName = "macosx" + archBits + "/" + trgName; } if(srcName!=null) { // create dll if necessary Resource binDir = getBinDirectory(config); Resource trg = binDir.getRealResource(trgName); if(!trg.exists() || trg.length()==0) { log.info("Instrumentation", "deploy /resource/bin/"+srcName+" to "+trg); InputStream src = InfoImpl.class.getResourceAsStream("/resource/bin/"+srcName); try { IOUtil.copy(src, trg,true); } catch (IOException e) { log.log(Log.LEVEL_INFO,"Instrumentation",e); } } // set directory to library path SystemUtil.addLibraryPathIfNoExist(binDir,log); } } /** * If <b>ibm</b> is false, this private method will create a new URLClassLoader and attempt to load the * com.sun.tools.attach.VirtualMachine class from the provided toolsJar file. * * <p> * If <b>ibm</b> is true, this private method will ignore the toolsJar parameter and load the * com.ibm.tools.attach.VirtualMachine class. * * * @return The AttachAPI VirtualMachine class <br> * or null if something unexpected happened. */ private static Class<?> loadVMClass(Resource toolsJar, Log log, JavaVendor vendor) { try { ClassLoader loader = ClassLoader.getSystemClassLoader(); String cls = vendor.getVirtualMachineClassName(); //if (!vendor.isIBM()) { loader = new URLClassLoader(new URL[] { ((FileResource)toolsJar).toURI().toURL() }, loader); //} return loader.loadClass(cls); } catch (Exception e) { log.log(Log.LEVEL_INFO,"Instrumentation",e); } return null; } }